/*
 * Copyright (c) 2013. wyouflf (wyouflf@gmail.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.lidroid.xutils.task;

import java.util.concurrent.Callable;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;

import android.os.Handler;
import android.os.Looper;
import android.os.Message;

/**
 * Author: wyouflf Date: 14-5-23 Time: 上午11:25
 */
public abstract class PriorityAsyncTask<Params, Progress, Result> implements TaskHandler {

	private static final int MESSAGE_POST_RESULT = 0x1;
	private static final int MESSAGE_POST_PROGRESS = 0x2;

	private static final InternalHandler sHandler = new InternalHandler();

	public static final Executor sDefaultExecutor = new PriorityExecutor();
	private final WorkerRunnable<Params, Result> mWorker;
	private final FutureTask<Result> mFuture;

	private volatile boolean mExecuteInvoked = false;

	private final AtomicBoolean mCancelled = new AtomicBoolean();
	private final AtomicBoolean mTaskInvoked = new AtomicBoolean();

	private Priority priority;

	public Priority getPriority() {
		return priority;
	}

	public void setPriority(Priority priority) {
		this.priority = priority;
	}

	/**
	 * Creates a new asynchronous task. This constructor must be invoked on the
	 * UI thread.
	 */
	public PriorityAsyncTask() {
		mWorker = new WorkerRunnable<Params, Result>() {
			public Result call() throws Exception {
				mTaskInvoked.set(true);

				android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
				// noinspection unchecked
				return postResult(doInBackground(mParams));
			}
		};

		mFuture = new FutureTask<Result>(mWorker) {
			@Override
			protected void done() {
				try {
					postResultIfNotInvoked(get());
				} catch (InterruptedException e) {
				} catch (ExecutionException e) {
					throw new RuntimeException("An error occured while executing doInBackground()", e.getCause());
				} catch (CancellationException e) {
					postResultIfNotInvoked(null);
				}
			}
		};
	}

	private void postResultIfNotInvoked(Result result) {
		final boolean wasTaskInvoked = mTaskInvoked.get();
		if (!wasTaskInvoked) {
			postResult(result);
		}
	}

	private Result postResult(Result result) {
		@SuppressWarnings("unchecked")
		Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult<Result>(this, result));
		message.sendToTarget();
		return result;
	}

	/**
	 * Override this method to perform a computation on a background thread. The
	 * specified parameters are the parameters passed to {@link #execute} by the
	 * caller of this task.
	 * This method can call {@link #publishProgress} to publish updates on the
	 * UI thread.
	 *
	 * @param params
	 *            The parameters of the task.
	 * @return A result, defined by the subclass of this task.
	 * @see #onPreExecute()
	 * @see #onPostExecute
	 * @see #publishProgress
	 */
	protected abstract Result doInBackground(Params... params);

	/**
	 * Runs on the UI thread before {@link #doInBackground}.
	 *
	 * @see #onPostExecute
	 * @see #doInBackground
	 */
	protected void onPreExecute() {
	}

	protected void onPostExecute(Result result) {
	}

	/**
	 * Runs on the UI thread after {@link #publishProgress} is invoked. The
	 * specified values are the values passed to {@link #publishProgress}.
	 *
	 * @param values
	 *            The values indicating progress.
	 * @see #publishProgress
	 * @see #doInBackground
	 */
	protected void onProgressUpdate(Progress... values) {
	}

	protected void onCancelled(Result result) {
		onCancelled();
	}


	protected void onCancelled() {
	}

	@Override
	public final boolean isCancelled() {
		return mCancelled.get();
	}

	public final boolean cancel(boolean mayInterruptIfRunning) {
		mCancelled.set(true);
		return mFuture.cancel(mayInterruptIfRunning);
	}

	@Override
	public boolean supportPause() {
		return false;
	}

	@Override
	public boolean supportResume() {
		return false;
	}

	@Override
	public boolean supportCancel() {
		return true;
	}

	@Override
	public void pause() {
	}

	@Override
	public void resume() {
	}

	@Override
	public void cancel() {
		this.cancel(true);
	}

	@Override
	public boolean isPaused() {
		return false;
	}

	/**
	 * Waits if necessary for the computation to complete, and then retrieves
	 * its result.
	 *
	 * @return The computed result.
	 * @throws java.util.concurrent.CancellationException
	 *             If the computation was cancelled.
	 * @throws java.util.concurrent.ExecutionException
	 *             If the computation threw an exception.
	 * @throws InterruptedException
	 *             If the current thread was interrupted while waiting.
	 */
	public final Result get() throws InterruptedException, ExecutionException {
		return mFuture.get();
	}

	/**
	 * Waits if necessary for at most the given time for the computation to
	 * complete, and then retrieves its result.
	 *
	 * @param timeout
	 *            Time to wait before cancelling the operation.
	 * @param unit
	 *            The time unit for the timeout.
	 * @return The computed result.
	 * @throws java.util.concurrent.CancellationException
	 *             If the computation was cancelled.
	 * @throws java.util.concurrent.ExecutionException
	 *             If the computation threw an exception.
	 * @throws InterruptedException
	 *             If the current thread was interrupted while waiting.
	 * @throws java.util.concurrent.TimeoutException
	 *             If the wait timed out.
	 */
	public final Result get(long timeout, TimeUnit unit)
			throws InterruptedException, ExecutionException, TimeoutException {
		return mFuture.get(timeout, unit);
	}

	/**
	 * @param params
	 *            The parameters of the task.
	 * @return This instance of AsyncTask.
	 * @throws IllegalStateException
	 *             If execute has invoked.
	 * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
	 * @see #execute(Runnable)
	 */
	public final PriorityAsyncTask<Params, Progress, Result> execute(Params... params) {
		return executeOnExecutor(sDefaultExecutor, params);
	}

	/**
	 * @param exec
	 *            The executor to use.
	 * @param params
	 *            The parameters of the task.
	 * @return This instance of AsyncTask.
	 * @throws IllegalStateException
	 *             If execute has invoked.
	 * @see #execute(Object[])
	 */
	public final PriorityAsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params) {
		if (mExecuteInvoked) {
			throw new IllegalStateException("Cannot execute task:" + " the task is already executed.");
		}

		mExecuteInvoked = true;

		onPreExecute();

		mWorker.mParams = params;
		exec.execute(new PriorityRunnable(priority, mFuture));

		return this;
	}

	/**
	 * Convenience version of {@link #execute(Object...)} for use with a simple
	 * Runnable object. See {@link #execute(Object[])} for more information on
	 * the order of execution.
	 *
	 * @see #execute(Object[])
	 * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
	 */
	public static void execute(Runnable runnable) {
		execute(runnable, Priority.DEFAULT);
	}

	/**
	 * Convenience version of {@link #execute(Object...)} for use with a simple
	 * Runnable object. See {@link #execute(Object[])} for more information on
	 * the order of execution.
	 *
	 * @see #execute(Object[])
	 * @see #executeOnExecutor(java.util.concurrent.Executor, Object[])
	 */
	public static void execute(Runnable runnable, Priority priority) {
		sDefaultExecutor.execute(new PriorityRunnable(priority, runnable));
	}

	protected final void publishProgress(Progress... values) {
		if (!isCancelled()) {
			sHandler.obtainMessage(MESSAGE_POST_PROGRESS, new AsyncTaskResult<Progress>(this, values)).sendToTarget();
		}
	}

	private void finish(Result result) {
		if (isCancelled()) {
			onCancelled(result);
		} else {
			onPostExecute(result);
		}
	}

	private static class InternalHandler extends Handler {

		private InternalHandler() {
			super(Looper.getMainLooper());
		}

		@SuppressWarnings("unchecked")
		@Override
		public void handleMessage(Message msg) {
			AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
			switch (msg.what) {
			case MESSAGE_POST_RESULT:
				// There is only one result
				result.mTask.finish(result.mData[0]);
				break;
			case MESSAGE_POST_PROGRESS:
				result.mTask.onProgressUpdate(result.mData);
				break;
			}
		}
	}

	private static abstract class WorkerRunnable<Params, Result> implements Callable<Result> {
		Params[] mParams;
	}

	@SuppressWarnings("rawtypes")
	private static class AsyncTaskResult<Data> {
		final PriorityAsyncTask mTask;
		final Data[] mData;

		AsyncTaskResult(PriorityAsyncTask task, Data... data) {
			mTask = task;
			mData = data;
		}
	}
}
